﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Lr
{
    /// <summary>
    /// Holds helper/utility methods for evaluating input and throwing appropriate exceptions.
    /// </summary>
    public static class ThrowIf
    {
        #region Common Private Functions
        /// <summary>
        /// Takes in an expression and returns the name of the member.
        /// </summary>
        /// <typeparam name="TMember">Type of the member.</typeparam>
        /// <param name="memberExpression">The expression.</param>
        /// <returns>A string holding the name of the member.</returns>
        private static string GetMemberName<TMember>(Expression<Func<TMember>> memberExpression)
        {
            try {
                // 01. Check if the memberExpression contains calls to properties or methods.
                var lambdaString = memberExpression.Body.ToString();
                var lastIndex = lambdaString.IndexOf(").") + 2;
                var lambaStringSimplified = lambdaString.Substring(lastIndex);

                // 01a. We think it contains calls to properties or other methods, try to return a simplified version of the lambda.
                if (lambaStringSimplified.Contains(".")) {
                    return lambaStringSimplified;
                }
                    // 01b. We think it doesn't contain a call to properties or other methods, try to return the member name itself.
                else {
                    var expressionBody = (MemberExpression)memberExpression.Body;
                    return expressionBody.Member.Name;
                }
            } catch (Exception ex) {
                var message = "Exception '{0}' occurred while trying to execute GetMemberName(). Try returning memberExpression.ToString().".FormatWith(ex.GetType().ToFriendlyName());
                return memberExpression.ToString();
            }
        }

        /// <summary>
        /// Helper method to throw a <typeparamref name="TException"/> <see cref="Exception"/>, and optionally log it.
        /// </summary>
        /// <typeparam name="TException">Type of the <see cref="Exception"/> to throw, and optionally log.</typeparam>
        /// <param name="parameterName">The parameter name to pass to the <typeparamref name="TException"/> <see cref="Exception"/>.</param>
        /// <param name="message">The message to pass to the <typeparamref name="TException"/> <see cref="Exception"/>.</param>        
        private static void Throw<TException>(string parameterName, string message, string fileName = "")
            where TException : Exception, new()
        {
            // 01. Gather information in variables
            var exceptionType = typeof(TException);
            var exception = new Exception();
            var constructors = new List<Tuple<int, ConstructorInfo>>();

            // 02. Find the different constructors for TException, and list the ones that can be used.
            foreach (var constructor in exceptionType.GetConstructors()) {
                var parameterNames = constructor
                    .GetParameters()
                    .AsQueryable()
                    .Where(p => p.ParameterType.Equals(typeof(String)))
                    .Select(p => p.Name.ToLower());

                if (parameterNames.Count() == 2) {
                    if (parameterNames.Contains("paramname") && parameterNames.Contains("message"))
                        constructors.Add(new Tuple<int, ConstructorInfo>(0, constructor));
                    if (parameterNames.Contains("message") && parameterNames.Contains("filename"))
                        constructors.Add(new Tuple<int, ConstructorInfo>(1, constructor));
                }
                if (parameterNames.Count() == 1 && parameterNames.Contains("message"))
                    constructors.Add(new Tuple<int, ConstructorInfo>(Int32.MinValue, constructor));
            }

            // 03a. Choose a constructor to use
            var exceptionIsChosen = false;
            if (constructors.Count != 0) {
                // parameterName, message
                if (constructors.Where(c => c.Item1 == 0).Count() != 0) {
                    var constructor = constructors.Where(c => c.Item1 == 0).Select(c => c.Item2).First();
                    exception = (constructor.Invoke(new object[] { parameterName, message }) as TException);
                    exceptionIsChosen = true;
                }

                // fileName, message
                if (!exceptionIsChosen && constructors.Where(c => c.Item1 == 1).Count() != 0) {
                    var constructor = constructors.Where(c => c.Item1 == 1).Select(c => c.Item2).First();
                    exception = (constructor.Invoke(new object[] { fileName, message }) as TException);
                    exceptionIsChosen = true;
                }

                // Generic one: message
                if (!exceptionIsChosen && constructors.Where(c => c.Item1 == Int32.MinValue).Count() != 0) {
                    var constructor = constructors.Where(c => c.Item1 == Int32.MinValue).Select(c => c.Item2).First();
                    exception = (constructor.Invoke(new object[] { message }) as TException);
                    exceptionIsChosen = true;
                }
            }
                // 03b. or use the standard NullReferenceException if no usable constructor was found.
            else {
                //exception = new TException();
                exception = new NullReferenceException(message);
            }

            // 04. Log
            // Trace.WriteLine("Throwing exception '{0}' with message: '{1}'.".FormatWith(exception.GetType().ToFriendlyName(), message), TRACE_CLASS_CATEGORY);

            // 05. Throw the exception
            throw exception;
        }
        #endregion Common Private Functions

        #region Null Checking On Objects
        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> is null, and if it is, it will throw an <see cref="ArgumentNullException"/>.
        /// </summary>
        /// <typeparam name="TParameter">The type of the <paramref name="parameterExpression"/>.</typeparam>
        /// <param name="parameterExpression">The parameter to check for null.</param>        
        public static void Null<TParameter>(Expression<Func<TParameter>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
        {
            ThrowIf.Null<TParameter, ArgumentNullException>(parameterExpression, callerMemberName, callerFilePath, callerLineNumber);
        }

        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> is null, and if it is, it will throw an <see cref="Exception"/> of the given <typeparamref name="TException"/> type.
        /// </summary>
        /// <typeparam name="TParameter">The type of the <paramref name="parameterExpression"/>.</typeparam>
        /// <typeparam name="TException">Type of the <see cref="Exception"/> to throw, and optionally log.</typeparam>
        /// <param name="parameterExpression">The parameter to check for null.</param>        
        public static void Null<TParameter, TException>(Expression<Func<TParameter>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
            where TException : Exception, new()
        {
            // 01. Determine whether to throw or not
            if (parameterExpression.Compile()() != null)
                return;

            // 02. Throw
            var parameterName = GetMemberName<TParameter>(parameterExpression);
            var message = "Parameter '{0}'{1}{2}{3}can not be null."
                .FormatWith(
                    parameterName,
                    String.IsNullOrEmpty(callerMemberName) ? "" : " in member '{0}' ".FormatWith(callerMemberName),
                    String.IsNullOrEmpty(callerFilePath) ? "" : " in file '{0}' ".FormatWith(callerFilePath),
                    (callerLineNumber.HasValue && String.IsNullOrEmpty(callerFilePath)) ? "" : " on line '{0}' ".FormatWith(callerLineNumber)
                )
                .RemoveMultipleWhitespaces();

            Throw<TException>(parameterName, message);
        }
        #endregion Null Checking On Objects

        #region Null, Empty And Whitespace Checking On Strings
        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> (of type <see cref="String"/>) is either null, empty or only contains whitespace, and if it is, it will throw an <see cref="ArgumentNullException"/>.
        /// </summary>
        /// <param name="parameterExpression">The <see cref="String"/> parameter to check for null, whitespace or empty.</param>        
        public static void NullOrEmpty(Expression<Func<string>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
        {
            NullOrEmpty<ArgumentNullException>(parameterExpression, callerMemberName, callerFilePath, callerLineNumber);
        }

        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> (of type <see cref="String"/>) is either null, empty or only contains whitespace, and if it is, it will throw an <see cref="Exception"/> of the given <typeparamref name="TException"/> type.
        /// </summary>
        /// <typeparam name="TException">Type of the <see cref="Exception"/> to throw, and optionally log.</typeparam>
        /// <param name="parameterExpression">The <see cref="String"/> parameter to check for null, whitespace or empty.</param>
        /// <param name="log">Optional <see cref="ILog"/> to write to.</param>
        public static void NullOrEmpty<TException>(Expression<Func<string>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
            where TException : Exception, new()
        {
            // 01. Determine whether to throw or not
            var value = parameterExpression.Compile()();

            var throwException = false;
            if (value == null)
                throwException = true;
            else
                throwException = String.IsNullOrEmpty(value.Trim());

            if (!throwException)
                return;

            // 02. Throw
            var parameterName = GetMemberName(parameterExpression);
            var message = "String parameter '{0}'{1}{2}{3}can not be null, empty or consist of only whitespace characters."
                .FormatWith(
                    parameterName,
                    String.IsNullOrEmpty(callerMemberName) ? "" : " in member '{0}' ".FormatWith(callerMemberName),
                    String.IsNullOrEmpty(callerFilePath) ? "" : " in file '{0}' ".FormatWith(callerFilePath),
                    (callerLineNumber.HasValue && String.IsNullOrEmpty(callerFilePath)) ? "" : " on line '{0}' ".FormatWith(callerLineNumber)
                )
                .RemoveMultipleWhitespaces();

            Throw<TException>(parameterName, message);
        }
        #endregion Null, Empty And Whitespace Checking On Strings

        #region IRange<T> OutOfRange / ArgumentOutOfRangeException Checking
        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> falls withing the minimum and maximum values specified in the given <paramref name="range"/>, if it doesn't it will throw an <see cref="ArgumentOutOfRangeException"/>.
        /// </summary>
        /// <typeparam name="TParameter">The type of the <paramref name="parameterExpression"/>.</typeparam>
        /// <param name="parameterExpression">The parameter to evaluate.</param>
        /// <param name="range">The <see cref="IRange"/> of <see cref="TParameter"/> used to evaluate the <paramref name="parameterExpression"/>.</param>        
        /// <param name="isMinimumIncluded">Determines whether the <see cref="Minimum"/> range is included in the comparison.</param>
        /// <param name="isMaximumIncluded">Determines whether the <see cref="Maximum"/> range is included in the comparison.</param>
        /// <exception cref="ArgumentNullException">When <paramref name="range"/> is null.</exception>
        public static void OutOfRange<TParameter>(Expression<Func<TParameter>> parameterExpression, IRange<TParameter> range, bool isMinimumIncluded = true, bool isMaximumIncluded = true, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
        {
            OutOfRange<TParameter, ArgumentOutOfRangeException>(parameterExpression, range, isMinimumIncluded, isMaximumIncluded, callerMemberName, callerFilePath, callerLineNumber);
        }

        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> falls withing the minimum and maximum values specified in the given <paramref name="range"/>, if it doesn't it will throw an <see cref="Exception"/> of the given <typeparamref name="TException"/> type.
        /// </summary>
        /// <typeparam name="TParameter">The type of the <paramref name="parameterExpression"/>.</typeparam>
        /// <typeparam name="TException">Type of the <see cref="Exception"/> to throw, and optionally log.</typeparam>
        /// <param name="parameterExpression">The parameter to evaluate.</param>
        /// <param name="range">The <see cref="IRange"/> of <see cref="TParameter"/> used to evaluate the <paramref name="parameterExpression"/>.</param>        
        /// <param name="isMinimumIncluded">Determines whether the <see cref="Minimum"/> range is included in the comparison.</param>
        /// <param name="isMaximumIncluded">Determines whether the <see cref="Maximum"/> range is included in the comparison.</param>
        /// <exception cref="ArgumentNullException">When <paramref name="range"/> is null.</exception>
        public static void OutOfRange<TParameter, TException>(Expression<Func<TParameter>> parameterExpression, IRange<TParameter> range, bool isMinimumIncluded = true, bool isMaximumIncluded = true, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
            where TException : Exception, new()
        {
            // 00. Input verification
            ThrowIf.Null(() => range);

            // 01. Determine whether to throw or not
            var value = parameterExpression.Compile()();
            var isInRange = range.IsInRange(value, isMinimumIncluded, isMaximumIncluded);

            if (isInRange)
                return;

            // 02. Throw
            var parameterName = GetMemberName(parameterExpression);
            var message = "Parameter '{0}'{1}{2}{3}appears to be out of range, was '{4}' and should be between '{5}' and '{6}', where the minimum value is {7} and maximum value is {8}."
                .FormatWith(
                    parameterName,
                    String.IsNullOrEmpty(callerMemberName) ? "" : " in member '{0}' ".FormatWith(callerMemberName),
                    String.IsNullOrEmpty(callerFilePath) ? "" : " in file '{0}' ".FormatWith(callerFilePath),
                    (callerLineNumber.HasValue && String.IsNullOrEmpty(callerFilePath)) ? "" : " on line '{0}' ".FormatWith(callerLineNumber),
                    value.ToString(),
                    range.Minimum.ToString(),
                    range.Maximum.ToString(),
                    isMinimumIncluded ? "included" : "excluded",
                    isMaximumIncluded ? "included" : "excluded"
                )
                .RemoveMultipleWhitespaces();

            Throw<TException>(parameterName, message);
        }
        #endregion IRange<T> OutOfRange / ArgumentOutOfRangeException Checking

        #region File Not Found
        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> holds a path/reference to a file that exists, if it doesn't it will throw an <see cref="FileNotFoundException"/>.
        /// </summary>
        /// <param name="parameterExpression">The parameter to evaluate.</param>        
        /// <exception cref="ArgumentNullException">When <paramref name="range"/> is null.</exception>
        public static void FileNotFound(Expression<Func<string>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
        {
            // 00. Input verification
            ThrowIf.Null(() => parameterExpression);

            // 01. Determine whether to throw or not
            var value = parameterExpression.Compile()();

            if (File.Exists(value))
                return;

            // 02. Throw and optionally log
            var parameterName = GetMemberName(parameterExpression);
            ProcessFileNotFound<FileNotFoundException>(parameterName, value, callerMemberName, callerFilePath, callerLineNumber);
        }

        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> has a file that exists, if it doesn't it will throw an <see cref="FileNotFoundException"/>.
        /// </summary>
        /// <param name="parameterExpression">The parameter to evaluate.</param>        
        /// <exception cref="ArgumentNullException">When <paramref name="range"/> is null.</exception>
        public static void FileNotFound(Expression<Func<FileInfo>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
        {
            FileNotFound<FileNotFoundException>(parameterExpression, callerMemberName, callerFilePath, callerLineNumber);
        }

        /// <summary>
        /// Determines whether the given <paramref name="parameterExpression"/> has a file that exists, if it doesn't it will throw an <see cref="Exception"/> of the given <typeparamref name="TException"/> type.
        /// </summary>
        /// <typeparam name="TException">Type of the <see cref="Exception"/> to throw, and optionally log.</typeparam>
        /// <param name="parameterExpression">The parameter to evaluate.</param>        
        /// <exception cref="ArgumentNullException">When <paramref name="range"/> is null.</exception>
        public static void FileNotFound<TException>(Expression<Func<FileInfo>> parameterExpression, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
            where TException : Exception, new()
        {
            // 00. Input verification
            ThrowIf.Null(() => parameterExpression);

            // 01. Determine whether to throw or not
            var value = parameterExpression.Compile()();

            if (value.Exists)
                return;

            // 02. Throw and optionally log
            var parameterName = GetMemberName(parameterExpression);
            ProcessFileNotFound<TException>(parameterName, value.FullName, callerMemberName, callerFilePath, callerLineNumber);
        }

        /// <summary>
        /// Helper method for the "FileNotFound" methods.
        /// </summary>
        /// <typeparam name="TException">Type of the <see cref="Exception"/> to throw, and optionally log.</typeparam>
        /// <param name="parameterName">The parameter name that holds the reference to the file.</param>
        /// <param name="fullFileName">Full file name and path to the file.</param>        
        private static void ProcessFileNotFound<TException>(string parameterName, string fullFileName, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int? callerLineNumber = null)
            where TException : Exception, new()
        {
            var message = "Parameter '{0}' {1}{2}{3} holds a reference to file '{4}' that appears non existent."
                .FormatWith(
                    parameterName,
                    String.IsNullOrEmpty(callerMemberName) ? "" : " in member '{0}' ".FormatWith(callerMemberName),
                    String.IsNullOrEmpty(callerFilePath) ? "" : " in file '{0}' ".FormatWith(callerFilePath),
                    (callerLineNumber.HasValue && String.IsNullOrEmpty(callerFilePath)) ? "" : " on line '{0}' ".FormatWith(callerLineNumber),
                    fullFileName
                )
                .RemoveMultipleWhitespaces();

            Throw<TException>(parameterName, message, fullFileName);
        }
        #endregion File Not Found
    }
}